<?php
require_once 'INumber.php';

class Number extends Object implements INumber, Serializable {
	public function __construct($number = 0) {
		$this->set($number);
	}

	public static function create ($number = 0) {
		return new self ($number);
	}

	public function __toString () {
		return (string) $this->atom;
	}

	// Get

	public function getString ($decimals = null, $decPoint = ',', $thousandsSep = '') {
		return new String (is_null($decimals) ? $this : number_format(
			$this->atom, $this->toNum($decimals), $decPoint, $thousandsSep
		));
	}

	public function get () {
		return $this->atom;
	}

	public function set ($number) {
		$this->atom = $this->toNum($number);
		return $this;
	}

	public function getInt () {
		return (int) $this->atom;
	}

	public function getFloat () {
		return (float) $this->atom;
	}

	// is

	public function isInfinite () {
		return is_infinite ($this->atom);
	}

	public function isFinite () {
		return is_finite ($this->atom);
	}

	public function isNaN () {
		return is_nan ($this->atom);
	}

	public function isFloat () {
		return is_float ($this->atom);
	}

	public function isInt () {
		return is_int ($this->atom);
	}

	public function times ($func) {
		for ($i = 0; $i < $this->atom; $i++) {
			$func(new Number($i));
		}
	}

	public function isOdd () {
		return !!($this->atom % 2);
	}

	public function isEven () {
		return !($this->atom % 2);
	}

	public function remainder ($num) {
		return $this->set($this->atom % $num);
	}

	public function dump ($plain = false) {
		if (class_exists('Dump')) {
			echo new Dump($this, $plain);
			return $this;
		} else {
			trigger_error ('Number::dump() require `Dump` class', E_USER_WARNING);
		}
	}

	// To/from hex/oct/bin
	public function hex () {
		return $this->numeration(func_get_args(), 'hexdec', 'dechex');
	}

	public function oct () {
		return $this->numeration(func_get_args(), 'octdec', 'decoct');
	}

	public function bin () {
		return $this->numeration(func_get_args(), 'bindec', 'decbin');
	}

	protected function numeration ($argv, $toDec, $decTo) {
		return $argv ?
			$this->set ($toDec(strval(  $argv[0] ))) :
			new String ($decTo(intval($this->atom))) ;
	}

	// Rand
	public function rand () {
		$num = func_num_args();
		if ($num == 2) {
			$min = func_get_arg(0);
			$max = func_get_arg(1);
		} else if ($num == 1) {
			$min = 0;
			$max = func_get_arg(0);
		} else {
			$min = 0;
			$max = mt_getrandmax();
		}
		return $this->set(mt_rand ($this->toNum($min), $this->toNum($max)));
	}

	public function seed ($seed) {
		mt_srand($this->toNum($seed));
		return $this;
	}

	// Compare
	public function gt ($num = 0, $including = false) {
		$num = $this->toNum($num);
		return ($including and $this->atom == $num) || $this->atom > $num;
	}

	public function lt ($num = 0, $including = false) {
		$num = $this->toNum($num);
		return ($including and $this->atom == $num) || $this->atom < $num;
	}

	public function equals ($num) {
		return $this->atom === $this->toNum($num);
	}

	// Primitive
	public function add () { // plus
		foreach (func_get_args() as $arg) {
			$this->atom += $this->toNum($arg);
		}
		return $this;
	}

	public function subtract () { // minus
		foreach (func_get_args() as $arg) {
			$this->atom -= $this->toNum($arg);
		}
		return $this;
	}

	public function multiply () { // times
		foreach (func_get_args() as $arg) {
			$this->atom *= $this->toNum($arg);
		}
		return $this;
	}

	public function divided () { // fraction, divide
		foreach (func_get_args() as $arg) {
			$this->atom /= $this->toNum($arg);
		}
		return $this;
	}

	// Sum
	const EVEN = 01;
	const ODD  = 02;
	const FULL = 03;

	/**
	 * self::EVEN: // 6->sum() == 12 == 2 + 4 + 6
	 * self::ODD:  // 6->sum() ==  9 == 1 + 3 + 5
	 * default:    // 6->sum() == 21 == 1 + 2 + 3 + 4 + 5 + 6
	 * @param int $mode
	 * @return Number
	 */
	public function sum ($mode = self::FULL) {
		$x = $this->atom;
		if ($x != 0) {
			$func = $mode == self::EVEN ? 'sumEven' :
				($mode == self::ODD ? 'sumOdd' : 'sumFull');
			return $this->set($this->$func($x));
		}
		return $this;
	}

	protected function sumOdd ($x) {
		if ($x % 2) {
			$x > 0 ? $x++ : $x--;
		}
		$y = pow($x/2, 2);
		return $x > 0 ? $y : -$y;
	}

	protected function sumEven ($x) {
		if ($x % 2) {
			$x += $x > 0 ? -1 : 1;
		}
		$y = ($x * ($x + 2))/4;
		return $x > 0 ? $y : $x-$y;
	}

	protected function sumFull ($x) {
		$y = $x * ($x+1) / 2;
		return ($x < 0) ? ($x - $y) : $y;
	}

	// Rounding
	const UP   = 'UP';
	const DOWN = 'DOWN';

	public function round ($mode = 0) {
		if ($mode == self::UP) {
			$this->atom = ceil ($this->atom);
		} else if ($mode == self::DOWN) {
			$this->atom = floor ($this->atom);
		} else {
			$this->atom = round ($this->atom, $this->toNum($mode));
		}
		return $this;
	}

	// Converting
	protected $convert = array (
		'Bytes' => array (
			'B'   => 1,
			'KiB' => 1024,
			'MiB' => 1048576,
			'GiB' => 1073741824,
			'TiB' => 1099511627776,
			'PiB' => 1125899906842624,
			'EiB' => 1152921504606846976,
			'ZiB' => 1180591620717411303424,
			'YiB' => 1208925819614629174706176
		),
		'Length' => array (
			'SeaMile' => 1852000000,
			'Mile'    => 1609344000,
			'Metr'    =>    1000000,
			'Yard'    =>     914400,
			'Foot'    =>     304800,
			'Pouce'   =>      27070,
			'Inch'    =>      25400,
		),
		'Time' => array (
			'Second'  => 1,
			'Minute'  => 60,
			'Hour'    => 3600,
			'Day'     => 86400,
			'Week'    => 604800,
			'Month28' => 2419200,
			'Month29' => 2505600,
			'Month'   => 2592000,
			'Month30' => 2592000,
			'Month31' => 2678400,
			'Year'	  => 31536000,
			'LeapYear'=> 31622400,
		)
	);

	public function convert ($type, $from, $to) {
		if (($t = $this->parseConvertPath($type))) {
			$val = $t['values'];
			if (isset($val[$from]) and isset($val[$to])) {
				$this->atom = $this->atom * $val[$from] / $val[$to];
			} else {
				$s = new String("No item «%s» in «$t[path]»");
				if (!isset($val[$from])) {
					throw new OOException ($s->copy()->format($from));
				}
				if (!isset($val[$to])) {
					throw new OOException ($s->copy()->format($to));
				}
			}
		}
		return $this;
	}

	protected function parseConvertPath ($type) {
		$typeParts = explode('.', $type);
		$t = $this->convert;
		$path = new Map;
		foreach ($typeParts as $part) {
			$path[] = $part;
			if (isset($t[$part])) {
				$t = $t[$part];
			} else {
				throw new OOException ('No such convert category: «' . $path->join('.') . '»');
			}
		}
		return array (
			'values' => $t,
			'path'	 => $path->join('.')
		);
	}

	// Math
	public function abs () {
		return $this->set(abs($this->atom));
	}

	public function power ($pow = 2) {
		return $this->set(pow($this->atom, $this->toNum($pow)));
	}

	public function root ($root = 2) {
		$root = $this->toNum($root);
		$this->atom =
			($root == 0) ? 1 : (
			($root == 1) ? $this->atom : (
			($root == 2) ? sqrt($this->atom) :
				pow ($this->atom, 1/$root)
			));
		return $this;
	}

	// Trigonometry
	public function sin () {
		return $this->func('sin');
	}
	public function cos () {
		return $this->func('cos');
	}
	public function tan () {
		return $this->func('tan');
	}
	public function ctan () {
		return $this->set(1 / tan ($this->atom));
	}
	public function asin () {
		return $this->func('asin');
	}
	public function acos () {
		return $this->func('acos');
	}
	public function atan () {
		return $this->func('atan');
	}
	public function actan () {
		return $this->set( atan (1 / $this->atom ));
	}
	public function sinh () {
		return $this->func('sinh');
	}
	public function cosh () {
		return $this->func('cosh');
	}
	public function tanh () {
		return $this->func('tanh');
	}
	public function asinh () {
		return $this->func('asinh');
	}
	public function acosh () {
		return $this->func('acosh');
	}
	public function atanh () {
		return $this->func('atanh');
	}

	//
	public function log ($base = M_E) {
		$base = $this->toNum($base);
		return $this->set(
			($base == 10) ? log10 ($this->atom) : log ($this->atom, $base)
		);
	}
	public function exp () {
		$this->func('exp');
	}

	// Serializable
	public function serialize() {
		return serialize($this->atom);
	}
	public function unserialize($data) {
		$this->atom = unserialize($data);
	}
}